/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.project;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;

import gjexer.TExceptionDialog;
import gjexer.TWindow;
import gjexer.event.TResizeEvent;

import tjide.build.CompileListener;
import tjide.build.CompileTask;
import tjide.debugger.Breakpoint;
import tjide.ui.TranquilApplication;

/**
 * Project represents a collection of compile targets and other resources
 * that comprise an application.
 */
public class Project {

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The XML factory.
     */
    private static DocumentBuilder domBuilder;

    /**
     * The UI top-level application.
     */
    private TranquilApplication application;

    /**
     * Desired window top position for the project window.
     */
    private int windowTop = 20;

    /**
     * Desired window left position for the project window.
     */
    private int windowLeft = 0;

    /**
     * Desired window height for the project window.
     */
    private int windowHeight = 20;

    /**
     * Desired window width for the project window.
     */
    private int windowWidth = 80;

    /**
     * The file associated with this project.
     */
    private File projectFile;

    /**
     * The project name.
     */
    private String name;

    /**
     * The project author.
     */
    private String author = "";

    /**
     * The project copyright/written by date.
     */
    private String date = "";

    /**
     * The project notes.
     */
    private String notes = "";

    /**
     * The project root directory name.
     */
    private String rootDir = "";

    /**
     * The build output directory name.
     */
    private String buildDir = "";

    /**
     * The source input directory name.
     */
    private String sourceDir = "";

    /**
     * The resources directory name.
     */
    private String resourcesDir = "";

    /**
     * The API docs directory name.
     */
    private String apiDocsDir = "";

    /**
     * The application type.
     */
    private AppType appType = AppType.CONSOLE;

    /**
     * The license.
     */
    private License license = License.getLicense("GPL-3");

    /**
     * The top-level root target for this project.
     */
    private ProjectTarget projectTarget;

    /**
     * The list of targets for this project.
     */
    private List<Target> targets = new ArrayList<Target>();

    /**
     * The list of jars for this project.
     */
    private List<String> jars = new ArrayList<String>();

    /**
     * If true, build a "fat" jar for this project.
     */
    private boolean buildFatJar = false;

    /**
     * The list of watches for this project.
     */
    private List<String> watches = new ArrayList<String>();

    /**
     * The compile thread runner.
     */
    private CompileRunner compileRunner;

    /**
     * CompileRunner runs a sequence of targets through a compile, make or
     * build.
     */
    private class CompileRunner implements Runnable, CompileListener {

        /**
         * The targets remaining to compile.
         */
        private List<Target> targets;

        /**
         * The listeners monitoring the compile.
         */
        private List<CompileListener> listeners;

        /**
         * If true, the compile has completed and failed.
         */
        private boolean compileFailed = false;

        /**
         * If true, the compile has completed and succeeded.
         */
        private boolean compileSucceeded = false;

        /**
         * If true, a file is being compiled.
         */
        private volatile boolean inCompile = true;

        /**
         * Public constructor.
         *
         * @param targets the targets to compile
         * @param listeners the listeners being notified of the compile
         */
        public CompileRunner(final List<Target> targets,
            final List<CompileListener> listeners) {

            this.targets = targets;
            this.listeners = listeners;
        }

        /**
         *  Run a sequence of targets through a compile.
         */
        public void run() {
            try {
                /*
                System.err.println("compile begin: " +
                    targets.size() + " targets");
                 */

                // Notify of start.
                for (CompileListener listener: listeners) {
                    listener.setCompileTask(null);
                    listener.setCompileBegin();
                }

                // Compile each target in sequence.
                while (targets.size() > 0) {
                    Target target = targets.remove(0);
                    target.setCompileListener(this);

                    if (compileFailed) {
                        break;
                    }

                    synchronized (this) {
                        // Start the compile.
                        inCompile = true;
                        // System.err.println("compile: " + target);
                        target.compile(Project.this);

                        // Wait for a compile success or failure before
                        // moving on.
                        while (inCompile) {
                            try {
                                // System.err.println("waiting for notify...");
                                this.wait();
                            } catch (InterruptedException e) {
                                // SQUASH
                            }
                        }
                    }

                } // while (targets.size() > 0)

                // System.err.println("Compile finished.");

                if ((compileFailed == false) && (compileSucceeded == false)) {
                    // There was nothing to do, so call that success.
                    compileSucceeded = true;
                }

                for (CompileListener listener: listeners) {
                    if (compileFailed) {
                        listener.setCompileFailed(true);
                    } else if (compileSucceeded) {
                        listener.setCompileSucceeded(true);
                    }
                }
            } catch (Exception e) {
                displayException(e);
            }
        }

        // --------------------------------------------------------------------
        // CompileListener ----------------------------------------------------
        // --------------------------------------------------------------------

        /**
         * Notify of a new warning message.  This will increment the warning
         * count.
         *
         * @param target the target that generated this message
         * @param line the line number of the message.  1-based: 0 or negative
         * means no relevant line number.
         * @param column the column number of the message.  1-based: 0 or
         * negative means no relevant column number.
         * @param message the message text
         */
        public void addCompileWarning(final Target target, final long line,
            final long column, final String message) {

            for (CompileListener listener: listeners) {
                listener.addCompileWarning(target, line, column, message);
            }
        }

        /**
         * Notify of a new error message.  This will increment the error count.
         *
         * @param target the target that generated this message
         * @param line the line number of the message.  1-based: 0 or negative
         * means no relevant line number.
         * @param column the column number of the message.  1-based: 0 or
         * negative means no relevant column number.
         * @param message the message text
         */
        public void addCompileError(final Target target, final long line,
            final long column, final String message) {

            for (CompileListener listener: listeners) {
                listener.addCompileError(target, line, column, message);
            }
        }

        /**
         * Notify a new error count.
         *
         * @param count the new count of errors
         */
        public void setCompileErrorCount(final int count) {
            for (CompileListener listener: listeners) {
                listener.setCompileErrorCount(count);
            }
        }

        /**
         * Notify a new warning count.
         *
         * @param count the new count of warnings
         */
        public void setCompileWarningCount(final int count) {
            for (CompileListener listener: listeners) {
                listener.setCompileWarningCount(count);
            }
        }

        /**
         * Notify a new line count.
         *
         * @param count the new count of lines
         */
        public void setCompileLineCount(final int count) {
            for (CompileListener listener: listeners) {
                listener.setCompileLineCount(count);
            }
        }

        /**
         * Notify a new available memory number.
         *
         * @param kbytes the number of kilobytes available
         */
        public void setCompileAvailableMemory(final long kbytes) {
            for (CompileListener listener: listeners) {
                listener.setCompileAvailableMemory(kbytes);
            }
        }

        /**
         * Notify of a compile beginning.
         */
        public void setCompileBegin() {
            // Ignore, this is called in run().
        }

        /**
         * Notify of a compile failure.
         *
         * @param completed if true, this is the final failure message
         */
        public void setCompileFailed(final boolean completed) {
            // System.err.println("xx Compile FAILED xx");

            compileFailed = true;

            for (CompileListener listener: listeners) {
                listener.setCompileFailed(false);
            }

            // Notify the run loop that this file is finished.
            synchronized (this) {
                inCompile = false;
                this.notifyAll();
            }
        }

        /**
         * Notify of a compile success.
         *
         * @param completed if true, this is the final success message
         */
        public void setCompileSucceeded(final boolean completed) {
            // System.err.println("-- Compile SUCCESS --");

            compileSucceeded = true;

            for (CompileListener listener: listeners) {
                listener.setCompileSucceeded(false);
            }

            // Notify the run loop that this file is finished.
            synchronized (this) {
                inCompile = false;
                this.notifyAll();
            }
        }

        /**
         * Notify of a file beginning the compile.
         *
         * @param source the source filename being compiled
         * @param destination the destination filename being compiled
         */
        public void setCompileFile(final String source, final String destination) {
            for (CompileListener listener: listeners) {
                listener.setCompileFile(source, destination);
            }
        }

        /**
         * Set the compile task callback interface, so that the CompileListener
         * can cancel if needed.
         *
         * @param compileTask the compile task
         */
        public void setCompileTask(final CompileTask compileTask) {
            for (CompileListener listener: listeners) {
                listener.setCompileTask(compileTask);
            }
        }

    }

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Public constructor.
     *
     * @param application the UI top-level application
     * @param file the project's XML file on disk
     * @param name the project name
     * @param rootDir the project root directory name
     * @param buildDir the project build directory name
     */
    public Project(final TranquilApplication application, final File file,
        final String name, final String rootDir,
        final String buildDir) {

        this.application        = application;
        this.projectFile        = file;
        this.name               = name;
        this.rootDir            = rootDir;
        this.buildDir           = buildDir;
        this.projectTarget      = new ProjectTarget(this);

        windowLeft              = 0;
        windowTop               = application.getDesktopBottom() - 11;
        windowWidth             = application.getScreen().getWidth();
        windowHeight            = 10;
    }

    /**
     * Construct from XML file.
     *
     * @param application the UI top-level application
     * @param filename the XML file to load
     * @throws IOException if a java.io operation throws
     * @throws ParserConfigurationException if no XML parser is available
     * @throws SAXException if XML parsing fails
     */
    public Project(final TranquilApplication application,
        final String filename) throws IOException,
                                      ParserConfigurationException,
                                      SAXException {

        this.application = application;

        if (domBuilder == null) {
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.
                                                                newInstance();
            domBuilder = dbFactory.newDocumentBuilder();
        }

        projectFile = new File(filename);
        rootDir = projectFile.getParent();
        if (rootDir == null) {
            // The project file is in the current directory.
            rootDir = ".";
        }

        Document doc = domBuilder.parse(projectFile);

        // Get the document's root XML node
        Node root = doc.getChildNodes().item(0);
        NodeList level1 = root.getChildNodes();
        for (int i = 0; i < level1.getLength(); i++) {
            Node node = level1.item(i);
            String name = node.getNodeName();
            String value = node.getTextContent();

            if (name.equals("name")) {
                this.name = value;
            }
            if (name.equals("author")) {
                this.author = value;
            }
            if (name.equals("date")) {
                this.date = value;
            }
            if (name.equals("notes")) {
                this.notes = value;
            }
            if (name.equals("license")) {
                this.license = License.getLicense(value);
                // Fallback to proprietary if unknown.
                if (this.license == null) {
                    this.license = License.getLicense("Proprietary");
                }
            }
            if (name.equals("appType")) {
                AppType [] appTypes = AppType.values();
                for (int j = 0; j < appTypes.length; j++) {
                    if (value.equals(appTypes[j].toString())) {
                        appType = appTypes[j];
                        break;
                    }
                }
            }
            if (name.equals("buildDir")) {
                this.buildDir = value;
            }
            if (name.equals("sourceDir")) {
                this.sourceDir = value;
            }
            if (name.equals("resourcesDir")) {
                this.resourcesDir = value;
            }
            if (name.equals("apiDocsDir")) {
                this.apiDocsDir = value;
            }
            if (name.equals("jars")) {
                NodeList jarList = node.getChildNodes();
                for (int j = 0; j < jarList.getLength(); j++) {
                    Node jarNode = jarList.item(j);
                    if (jarNode.getNodeName().equals("jar")) {
                        String jar = jarNode.getTextContent();
                        jars.add(jar);
                    }
                }
            }
            if (name.equals("buildFatJar")) {
                this.buildFatJar = value.equals("true");
            }
            if (name.equals("targets")) {
                NodeList targets = node.getChildNodes();
                for (int j = 0; j < targets.getLength(); j++) {
                    Node target = targets.item(j);
                    String targetType = target.getNodeName();
                    if (targetType.equals("javaTarget")) {
                        addJavaTarget(target);
                    }
                    if (targetType.equals("textTarget")) {
                        addTextTarget(target);
                    }
                    if (targetType.equals("jarTarget")) {
                        addJarTarget(target);
                    }
                }
            }
            if (name.equals("watches")) {
                NodeList watchList = node.getChildNodes();
                for (int j = 0; j < watchList.getLength(); j++) {
                    Node watchNode = watchList.item(j);
                    if (watchNode.getNodeName().equals("watch")) {
                        String watch = watchNode.getTextContent();
                        watches.add(watch);
                    }
                }
            }
            if (name.equals("windowPosition")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("top")) {
                        try {
                            windowTop = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("left")) {
                        try {
                            windowLeft = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("width")) {
                        try {
                            windowWidth = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("height")) {
                        try {
                            windowHeight = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                }
            }
        }

        this.projectTarget = new ProjectTarget(this);
    }

    // ------------------------------------------------------------------------
    // Project ----------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Get desired window top position for the project window.
     *
     * @return desired window top position
     */
    public int getWindowTop() {
        return windowTop;
    }

    /**
     * Set desired window top position for the project window.
     *
     * @param windowTop desired window top position
     */
    public void setWindowTop(final int windowTop) {
        this.windowTop = windowTop;
    }

    /**
     * Desired window left position for the project window.
     *
     * @return desired window left position for the project window
     */
    public int getWindowLeft() {
        return windowLeft;
    }

    /**
     * Set desired window left position for the project window.
     *
     * @param windowLeft desired window left position
     */
    public void setWindowLeft(final int windowLeft) {
        this.windowLeft = windowLeft;
    }

    /**
     * Desired window height for the project window.
     *
     * @return desired window height for the project window
     */
    public int getWindowHeight() {
        return windowHeight;
    }

    /**
     * Set desired window height for the project window.
     *
     * @param windowHeight desired window height
     */
    public void setWindowHeight(final int windowHeight) {
        this.windowHeight = windowHeight;
    }

    /**
     * Desired window width for the project window.
     *
     * @return desired window width for the project window
     */
    public int getWindowWidth() {
        return windowWidth;
    }

    /**
     * Set desired window width for the project window.
     *
     * @param windowWidth desired window width
     */
    public void setWindowWidth(final int windowWidth) {
        this.windowWidth = windowWidth;
    }

    /**
     * Get the project name.
     *
     * @return the project name
     */
    public String getName() {
        return name;
    }

    /**
     * Set the project name.
     *
     * @param name the project name
     */
    public void setName(final String name) {
        this.name = name;
        projectTarget.setName(name);
    }

    /**
     * Get the project author.
     *
     * @return the project author
     */
    public String getAuthor() {
        return author;
    }

    /**
     * Set the project author.
     *
     * @param author the project author
     */
    public void setAuthor(final String author) {
        this.author = author;
    }

    /**
     * Get the project copyright/written by date.
     *
     * @return the project copyright/written by date
     */
    public String getDate() {
        return date;
    }

    /**
     * Set the project copyright/written by date.
     *
     * @param date the project copyright/written by date
     */
    public void setDate(final String date) {
        this.date = date;
    }

    /**
     * Get the project notes.
     *
     * @return the project notes
     */
    public String getNotes() {
        return notes;
    }

    /**
     * Set the project notes.
     *
     * @param notes the project notes
     */
    public void setNotes(final String notes) {
        this.notes = notes;
    }

    /**
     * Get the project license.
     *
     * @return the project license
     */
    public License getLicense() {
        return license;
    }

    /**
     * Set the project license.
     *
     * @param license the project license
     */
    public void setLicense(final License license) {
        this.license = license;
    }

    /**
     * Set the project license.
     *
     * @param license the short name of the project license
     */
    public void setLicense(final String license) {
        this.license = License.getLicense(license);
        // Fallback to proprietary if unknown.
        if (this.license == null) {
            this.license = License.getLicense("Proprietary");
        }
    }

    /**
     * Get the project application type.
     *
     * @return the project application type
     */
    public AppType getAppType() {
        return appType;
    }

    /**
     * Set the project application type.
     *
     * @param appType the project application type
     */
    public void setAppType(final AppType appType) {
        this.appType = appType;
    }

    /**
     * Set the project application type.
     *
     * @param appType the name of project application type
     */
    public void setAppType(final String appType) {
        this.appType = Project.findAppType(appType);
    }

    /**
     * Find a project application type enum from string.
     *
     * @param appType the name of project application type
     * @return the application type, or AppType.CONSOLE if appType is not
     * found
     */
    public static AppType findAppType(final String appType) {
        if (appType.toLowerCase().equals("jexer (mit)")) {
            return AppType.JEXER;
        }
        if (appType.toLowerCase().equals("jexer (gpl)")) {
            return AppType.GJEXER;
        }

        AppType [] appTypes = AppType.values();
        for (int i = 0; i < appTypes.length; i++) {
            if (appType.toLowerCase().equals(appTypes[i].toString().toLowerCase())) {
                return appTypes[i];
            }
        }
        // Default to console app if not found.
        return AppType.CONSOLE;
    }

    /**
     * Get the project root directory name.
     *
     * @return the project root directory name
     */
    public String getRootDir() {
        return rootDir;
    }

    /**
     * Set the project root directory name.
     *
     * @param rootDir the project root directory name
     */
    public void setRootDir(final String rootDir) {
        if (rootDir.trim().length() == 0) {
            this.rootDir = ".";
        } else {
            this.rootDir = rootDir;
        }
        projectFile = new File(this.rootDir, projectFile.getName());
    }

    /**
     * Get the build directory name.
     *
     * @return the build directory name
     */
    public String getBuildDir() {
        return buildDir;
    }

    /**
     * Set the build directory name.
     *
     * @param buildDir the build directory name
     */
    public void setBuildDir(final String buildDir) {
        if (buildDir.trim().length() == 0) {
            this.buildDir = ".";
        } else {
            this.buildDir = buildDir;
        }
    }

    /**
     * Get the source directory name.
     *
     * @return the source directory name
     */
    public String getSourceDir() {
        return sourceDir;
    }

    /**
     * Set the source directory name.
     *
     * @param sourceDir the source directory name
     */
    public void setSourceDir(final String sourceDir) {
        if (sourceDir.trim().length() == 0) {
            this.sourceDir = ".";
        } else {
            this.sourceDir = sourceDir;
        }
    }

    /**
     * Get the root/source directory name.
     *
     * @return the root/source directory name
     */
    public String getFullSourceDir() {
        return new File(rootDir, sourceDir).toString();
    }

    /**
     * Get the root/build directory name.
     *
     * @return the root/build directory name
     */
    public String getFullBuildDir() {
        return new File(rootDir, buildDir).toString();
    }

    /**
     * Get the root/resources directory name.
     *
     * @return the root/resources directory name
     */
    public String getFullResourcesDir() {
        return new File(rootDir, resourcesDir).toString();
    }

    /**
     * Get the resources directory name.
     *
     * @return the resources directory name
     */
    public String getResourcesDir() {
        return resourcesDir;
    }

    /**
     * Set the resources directory name.
     *
     * @param resourcesDir the resources directory name
     */
    public void setResourcesDir(final String resourcesDir) {
        if (resourcesDir.trim().length() == 0) {
            this.resourcesDir = ".";
        } else {
            this.resourcesDir = resourcesDir;
        }
    }

    /**
     * Get the API docs directory name.
     *
     * @return the API docs directory name
     */
    public String getApiDocsDir() {
        return apiDocsDir;
    }

    /**
     * Set the API docs directory name.
     *
     * @param apiDocsDir the API docs directory name
     */
    public void setApiDocsDir(final String apiDocsDir) {
        if (apiDocsDir.trim().length() == 0) {
            this.apiDocsDir = ".";
        } else {
            this.apiDocsDir = apiDocsDir;
        }
    }

    /**
     * Get a copy of the jars list.
     *
     * @return a copy of the jars list
     */
    public List<String> getJars() {
        return new ArrayList<String>(jars);
    }

    /**
     * Set the jars list.
     *
     * @param jars the new jar list
     */
    public void setJars(final List<String> jars) {
        this.jars = new ArrayList<String>(jars);
    }

    /**
     * Add a jar to the jars list.
     *
     * @param jar the new jar to add
     */
    public void addJar(final String jar) {
        jars.add(jar);
    }

    /**
     * Get the build fat jar option.
     *
     * @return if true, the project will built into a single jar (fat jar)
     */
    public boolean getBuildFatJar() {
        return buildFatJar;
    }

    /**
     * Set the build fat jar option.
     *
     * @param buildFatJar if true, the project will built into a single jar
     * (fat jar)
     */
    public void setBuildFatJar(final boolean buildFatJar) {
        this.buildFatJar = buildFatJar;
    }

    /**
     * Get a (shallow) copy of the targets list.
     *
     * @return a copy of the targets
     */
    public List<Target> getTargets() {
        return new ArrayList<Target>(targets);
    }

    /**
     * Remove a target from the targets list.
     *
     * @param target target to remove
     */
    public void removeTarget(Target target) {
        targets.remove(target);
    }

    /**
     * Get an option value.
     *
     * @param key name of the option
     * @return the option value
     */
    public String getOption(final String key) {
        return application.getOption(key);
    }

    /**
     * Save the project to projectFile.
     */
    public void save() {
        try {
            saveProject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Make the project.
     */
    public void make() {
        // Make not-archives first, then archives afterwards.
        List<Target> compileTargets = new ArrayList<Target>();
        List<Target> archiveTargets = new ArrayList<Target>();
        String fileSourceDir = (new File(rootDir, sourceDir)).getPath();
        String fileBuildDir = (new File(rootDir, buildDir)).getPath();

        for (Target target: targets) {
            if (target.isStale(fileSourceDir, fileBuildDir)
                && !(target instanceof ArchiveTarget)
            ) {
                compileTargets.add(target);
            } else if (target instanceof ArchiveTarget) {
                archiveTargets.add(target);
            }
        }
        for (Target target: archiveTargets) {
            if (target.isStale(rootDir, buildDir)) {
                compileTargets.add(target);
            }
        }

        List<CompileListener> listeners = new ArrayList<CompileListener>();
        listeners.add(application.getCompileListener());
        CompileRunner compileRunner = new CompileRunner(compileTargets,
            listeners);

        (new Thread(compileRunner)).start();
    }

    /**
     * Rebuild the project, regardless of staleness.
     */
    public void buildAll() {
        // Make not-archives first, then archives afterwards.
        List<Target> compileTargets = new ArrayList<Target>();
        List<Target> archiveTargets = new ArrayList<Target>();
        String fileSourceDir = (new File(rootDir, sourceDir)).getPath();
        String fileBuildDir = (new File(rootDir, buildDir)).getPath();

        for (Target target: targets) {
            if (target instanceof ArchiveTarget) {
                archiveTargets.add(target);
            } else {
                compileTargets.add(target);
            }
        }
        for (Target target: archiveTargets) {
            compileTargets.add(target);
        }

        List<CompileListener> listeners = new ArrayList<CompileListener>();
        listeners.add(application.getCompileListener());
        CompileRunner compileRunner = new CompileRunner(compileTargets,
            listeners);

        (new Thread(compileRunner)).start();
    }

    /**
     * See if a source file is already in this project.
     *
     * @param sourceFilename the source filename, relative to the project's
     * source directory
     * @return true if the source file is already in the project
     */
    public boolean hasFileTarget(final String sourceFilename) {
        for (Target target: targets) {
            if (target instanceof FileTarget) {
                if (((FileTarget) target).getName().equals(sourceFilename)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Add a Java source file to this project.
     *
     * @param sourceFilename the source filename, relative to the project's
     * source directory
     * @return the new target
     */
    public JavaTarget addJavaTarget(final String sourceFilename) {
        JavaTarget target = new JavaTarget(this, sourceFilename);
        targets.add(target);
        return target;
    }

    /**
     * Add a text file to this project.
     *
     * @param sourceFilename the source filename, relative to the project's
     * source directory
     * @return the new target
     */
    public TextFileTarget addTextTarget(final String sourceFilename) {
        TextFileTarget target = new TextFileTarget(sourceFilename);
        targets.add(target);
        return target;
    }

    /**
     * Add a JAR file to this project.
     *
     * @param jarFilename the JAR filename, relative to the project's source
     * directory
     * @return the new target
     */
    public JarTarget addJarTarget(final String jarFilename) {
        JarTarget target = new JarTarget(jarFilename);
        targets.add(target);
        return target;
    }

    /**
     * Add a Java source file to this project.
     *
     * @param xmlNode the javaTarget XML node
     * @throws IOException if a java.io operation throws
     */
    private void addJavaTarget(final Node xmlNode) throws IOException {
        boolean openInEditor = false;
        boolean runnable = false;
        int editorWindowTop = 1;
        int editorWindowLeft = 0;
        int editorWindowWidth = 80;
        int editorWindowHeight = 20;

        String sourceFilename = "";
        List<Integer> breakpoints = new ArrayList<Integer>();
        List<String> runArguments = new ArrayList<String>();
        List<String> jvmRunArguments = new ArrayList<String>();

        NamedNodeMap attributes = xmlNode.getAttributes();
        if (attributes != null) {
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                if (attr.getNodeName().equals("openInEditor")) {
                    if (attr.getNodeValue().equals("true")) {
                        openInEditor = true;
                    }
                }
                if (attr.getNodeName().equals("runnable")) {
                    if (attr.getNodeValue().equals("true")) {
                        runnable = true;
                    }
                }
            }
        }

        NodeList level2 = xmlNode.getChildNodes();
        for (int i = 0; i < level2.getLength(); i++) {
            Node node = level2.item(i);
            String nodeName = node.getNodeName();
            String nodeValue = node.getTextContent();
            if (nodeName.equals("sourceFilename")) {
                sourceFilename = nodeValue;
            }
            if (nodeName.equals("breakpoints")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("line")) {
                        try {
                            breakpoints.add(Integer.parseInt(node3Value));
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                }
            }
            if (nodeName.equals("windowPosition")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("top")) {
                        try {
                            editorWindowTop = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("left")) {
                        try {
                           editorWindowLeft = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("width")) {
                        try {
                            editorWindowWidth = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("height")) {
                        try {
                            editorWindowHeight = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                }
            }

            if (nodeName.equals("runArguments")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("arg")) {
                        runArguments.add(node3Value);
                    }
                }
            }

            if (nodeName.equals("jvmRunArguments")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("jvmArg")) {
                        jvmRunArguments.add(node3Value);
                    }
                }
            }
        }

        JavaTarget target = new JavaTarget(this, sourceFilename);
        target.addCompileListener(application.getCompileListener());
        targets.add(target);
        for (int line: breakpoints) {
            target.addBreakpoint(line);
        }
        target.setRunnable(runnable);
        target.setRunArguments(runArguments);
        target.setJvmRunArguments(jvmRunArguments);

        if (openInEditor) {
            TWindow editor = application.openEditor(this, target);
            target.setWindow(editor);
            editor.setX(editorWindowLeft);
            editor.setY(editorWindowTop);
            editor.setWidth(editorWindowWidth);
            editor.setHeight(editorWindowHeight);
            editor.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
                    editorWindowWidth, editorWindowHeight));
        }

    }

    /**
     * Add a text file to this project.
     *
     * @param xmlNode the textTarget XML node
     * @throws IOException if a java.io operation throws
     */
    private void addTextTarget(final Node xmlNode) throws IOException {
        boolean openInEditor = false;
        int editorWindowTop = 1;
        int editorWindowLeft = 0;
        int editorWindowWidth = 80;
        int editorWindowHeight = 20;

        String sourceFilename = "";

        NamedNodeMap attributes = xmlNode.getAttributes();
        if (attributes != null) {
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                if (attr.getNodeName().equals("openInEditor")) {
                    if (attr.getNodeValue().equals("true")) {
                        openInEditor = true;
                    }
                }
            }
        }

        NodeList level2 = xmlNode.getChildNodes();
        for (int i = 0; i < level2.getLength(); i++) {
            Node node = level2.item(i);
            String nodeName = node.getNodeName();
            String nodeValue = node.getTextContent();
            if (nodeName.equals("sourceFilename")) {
                sourceFilename = nodeValue;
            }
            if (nodeName.equals("windowPosition")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("top")) {
                        try {
                            editorWindowTop = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("left")) {
                        try {
                           editorWindowLeft = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("width")) {
                        try {
                            editorWindowWidth = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                    if (node3Name.equals("height")) {
                        try {
                            editorWindowHeight = Integer.parseInt(node3Value);
                        } catch (NumberFormatException e) {
                            // SQUASH
                        }
                    }
                }
            }
        }

        TextFileTarget target = new TextFileTarget(sourceFilename);
        targets.add(target);

        if (openInEditor) {
            TWindow editor = application.openEditor(this, target);
            target.setWindow(editor);
            editor.setX(editorWindowLeft);
            editor.setY(editorWindowTop);
            editor.setWidth(editorWindowWidth);
            editor.setHeight(editorWindowHeight);
            editor.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
                    editorWindowWidth, editorWindowHeight));
        }

    }

    /**
     * Add a JAR target to this project.
     *
     * @param xmlNode the jarTarget XML node
     * @throws IOException if a java.io operation throws
     */
    private void addJarTarget(final Node xmlNode) throws IOException {
        String jarFilename = "";
        String mainClass = "";
        String implementationVersion = "";
        List<String> filters = new ArrayList<String>();

        NodeList level2 = xmlNode.getChildNodes();
        for (int i = 0; i < level2.getLength(); i++) {
            Node node = level2.item(i);
            String nodeName = node.getNodeName();
            String nodeValue = node.getTextContent();
            if (nodeName.equals("jarFilename")) {
                jarFilename = nodeValue;
            }
            if (nodeName.equals("Main-Class")) {
                mainClass = nodeValue;
            }
            if (nodeName.equals("Implementation-Version")) {
                implementationVersion = nodeValue;
            }
            if (nodeName.equals("includes")) {
                NodeList level3 = node.getChildNodes();
                for (int j = 0; j < level3.getLength(); j++) {
                    Node node3 = level3.item(j);
                    String node3Name = node3.getNodeName();
                    String node3Value = node3.getTextContent();
                    if (node3Name.equals("filter")) {
                        filters.add(node3Value);
                    }
                }
            }
        }

        JarTarget target = new JarTarget(jarFilename);
        targets.add(target);
        for (String filter: filters) {
            target.addFilter(filter);
        }
        if (mainClass.length() > 0) {
            target.setMainClass(mainClass);
        }
        if (implementationVersion.length() > 0) {
            target.setImplementationVersion(implementationVersion);
        }
    }

    /**
     * Create a DOM element representing a JavaTarget.
     *
     * @param doc the Document
     * @param target the Java target
     */
    private Element createJavaTargetXml(Document doc, JavaTarget target) {
        Element root = doc.createElement("javaTarget");
        TWindow editor = application.findInternalEditor(target);

        if (editor != null) {
            root.setAttribute("openInEditor", "true");
        }
        root.setAttribute("runnable", target.isRunnable() ? "true" : "false");

        Element elem = doc.createElement("sourceFilename");
        elem.appendChild(doc.createTextNode(target.getName()));
        root.appendChild(elem);

        Element breakpointList = doc.createElement("breakpoints");
        for (Breakpoint breakpoint: target.getBreakpoints()) {
            elem = doc.createElement("line");
            elem.appendChild(doc.createTextNode(
                Integer.toString(breakpoint.getLine())));
            breakpointList.appendChild(elem);
        }
        root.appendChild(breakpointList);

        if (editor != null) {
            Element windowList = doc.createElement("windowPosition");
            elem = doc.createElement("left");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getX())));
            windowList.appendChild(elem);
            elem = doc.createElement("top");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getY())));
            windowList.appendChild(elem);
            elem = doc.createElement("width");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getWidth())));
            windowList.appendChild(elem);
            elem = doc.createElement("height");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getHeight())));
            windowList.appendChild(elem);
            root.appendChild(windowList);
        }

        Element runArgsList = doc.createElement("runArguments");
        for (String arg: target.getRunArguments()) {
            elem = doc.createElement("arg");
            elem.appendChild(doc.createTextNode(arg));
            runArgsList.appendChild(elem);
        }
        root.appendChild(runArgsList);

        Element jvmRunArgsList = doc.createElement("jvmRunArguments");
        for (String jvmArg: target.getJvmRunArguments()) {
            elem = doc.createElement("jvmArg");
            elem.appendChild(doc.createTextNode(jvmArg));
            jvmRunArgsList.appendChild(elem);
        }
        root.appendChild(jvmRunArgsList);

        return root;
    }

    /**
     * Create a DOM element representing a TextFileTarget.
     *
     * @param doc the Document
     * @param target the Java target
     */
    private Element createTextTargetXml(Document doc, TextFileTarget target) {
        Element root = doc.createElement("textTarget");
        TWindow editor = application.findInternalEditor(target);

        if (editor != null) {
            root.setAttribute("openInEditor", "true");
        }

        Element elem = doc.createElement("sourceFilename");
        elem.appendChild(doc.createTextNode(target.getName()));
        root.appendChild(elem);

        if (editor != null) {
            Element windowList = doc.createElement("windowPosition");
            elem = doc.createElement("left");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getX())));
            windowList.appendChild(elem);
            elem = doc.createElement("top");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getY())));
            windowList.appendChild(elem);
            elem = doc.createElement("width");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getWidth())));
            windowList.appendChild(elem);
            elem = doc.createElement("height");
            elem.appendChild(doc.createTextNode(
                Integer.toString(editor.getHeight())));
            windowList.appendChild(elem);
            root.appendChild(windowList);
        }

        return root;
    }

    /**
     * Create a DOM element representing a JarTarget.
     *
     * @param doc the Document
     * @param target the JAR target
     */
    private Element createJarTargetXml(Document doc, JarTarget target) {
        Element root = doc.createElement("jarTarget");

        Element elem = doc.createElement("jarFilename");
        elem.appendChild(doc.createTextNode(target.getName()));
        root.appendChild(elem);

        String mainClass = target.getMainClass().trim();
        if (mainClass.length() > 0) {
            elem = doc.createElement("Main-Class");
            elem.appendChild(doc.createTextNode(mainClass));
            root.appendChild(elem);
        }

        String implementationVersion = target.getImplementationVersion().trim();
        if (implementationVersion.length() > 0) {
            elem = doc.createElement("Implementation-Version");
            elem.appendChild(doc.createTextNode(implementationVersion));
            root.appendChild(elem);
        }

        Element includeList = doc.createElement("includes");
        for (String filter: target.getFilters()) {
            elem = doc.createElement("filter");
            elem.appendChild(doc.createTextNode(filter));
            includeList.appendChild(elem);
        }
        root.appendChild(includeList);

        return root;
    }

    /**
     * Save the project to projectFile.
     */
    private void saveProject() throws ClassNotFoundException,
                                      IllegalAccessException,
                                      InstantiationException, IOException,
                                      ParserConfigurationException,
                                      SAXException {

        File file = projectFile;

        /*
         * For testing persistence, save to another filename.
         */
        // File file = new File(projectFile.getPath() + "_test");

        if (domBuilder == null) {
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.
                                                                newInstance();
            domBuilder = dbFactory.newDocumentBuilder();
        }
        Document doc = domBuilder.newDocument();
        doc.setXmlStandalone(true);
        Element root = doc.createElement("project");
        doc.appendChild(root);
        Element elem = doc.createElement("name");
        elem.appendChild(doc.createTextNode(this.name));
        root.appendChild(elem);
        elem = doc.createElement("author");
        elem.appendChild(doc.createTextNode(this.author));
        root.appendChild(elem);
        elem = doc.createElement("date");
        elem.appendChild(doc.createTextNode(this.date));
        root.appendChild(elem);
        elem = doc.createElement("notes");
        elem.appendChild(doc.createTextNode(this.notes));
        root.appendChild(elem);
        elem = doc.createElement("license");
        elem.appendChild(doc.createTextNode(this.license.getName()));
        root.appendChild(elem);
        elem = doc.createElement("appType");
        elem.appendChild(doc.createTextNode(this.appType.toString()));
        root.appendChild(elem);
        elem = doc.createElement("buildDir");
        elem.appendChild(doc.createTextNode(this.buildDir));
        root.appendChild(elem);
        elem = doc.createElement("sourceDir");
        elem.appendChild(doc.createTextNode(this.sourceDir));
        root.appendChild(elem);
        elem = doc.createElement("resourcesDir");
        elem.appendChild(doc.createTextNode(this.resourcesDir));
        root.appendChild(elem);
        elem = doc.createElement("apiDocsDir");
        elem.appendChild(doc.createTextNode(this.apiDocsDir));
        root.appendChild(elem);

        Element windowList = doc.createElement("windowPosition");
        elem = doc.createElement("left");
        elem.appendChild(doc.createTextNode(Integer.toString(windowLeft)));
        windowList.appendChild(elem);
        elem = doc.createElement("top");
        elem.appendChild(doc.createTextNode(Integer.toString(windowTop)));
        windowList.appendChild(elem);
        elem = doc.createElement("width");
        elem.appendChild(doc.createTextNode(Integer.toString(windowWidth)));
        windowList.appendChild(elem);
        elem = doc.createElement("height");
        elem.appendChild(doc.createTextNode(Integer.toString(windowHeight)));
        windowList.appendChild(elem);
        root.appendChild(windowList);

        Element jarList = doc.createElement("jars");
        for (String jar: this.jars) {
            elem = doc.createElement("jar");
            elem.appendChild(doc.createTextNode(jar));
            jarList.appendChild(elem);
        }
        root.appendChild(jarList);
        elem = doc.createElement("buildFatJar");
        elem.appendChild(doc.createTextNode(this.buildFatJar == true
                ? "true" : "false"));
        root.appendChild(elem);

        Element targets = doc.createElement("targets");
        for (Target target: this.targets) {
            if (target instanceof JavaTarget) {
                targets.appendChild(createJavaTargetXml(doc,
                        (JavaTarget) target));
            }
            if (target instanceof TextFileTarget) {
                targets.appendChild(createTextTargetXml(doc,
                        (TextFileTarget) target));
            }
            if (target instanceof JarTarget) {
                targets.appendChild(createJarTargetXml(doc,
                        (JarTarget) target));
            }
        }
        root.appendChild(targets);

        Element watchList = doc.createElement("watches");
        for (String watch: this.watches) {
            elem = doc.createElement("watch");
            elem.appendChild(doc.createTextNode(watch));
            watchList.appendChild(elem);
        }
        root.appendChild(watchList);

        DOMImplementationRegistry reg = DOMImplementationRegistry.newInstance();
        DOMImplementationLS impl = (DOMImplementationLS) reg.
                                                getDOMImplementation("LS");
        LSSerializer ls = impl.createLSSerializer();

        /*
        for (int i = 0; i < ls.getDomConfig().getParameterNames().getLength();
             i++) {
            System.err.println(ls.getDomConfig().getParameterNames().item(i));
        }
        */

        ls.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
        LSOutput lsOut = impl.createLSOutput();
        lsOut.setEncoding("UTF-8");
        FileWriter output = new FileWriter(file);
        lsOut.setCharacterStream(output);
        ls.write(doc, lsOut);

        output.close();
    }

    /**
     * Get the root project target for this project.
     *
     * @return the root project target
     */
    public ProjectTarget getProjectTarget() {
        return projectTarget;
    }

    /**
     * Get all breakpoints in this project.
     *
     * @return the list of breakpoints
     */
    public List<Breakpoint> getBreakpoints() {
        List<Breakpoint> breakpoints = new ArrayList<Breakpoint>();
        for (Target target: targets) {
            if (target instanceof FileTarget) {
                breakpoints.addAll(((FileTarget) target).getBreakpoints());
            }
        }
        return breakpoints;
    }

    /**
     * Substitute year, author, and program name tags in license text.
     *
     * @param line a line of license text
     * @return the line with substitution
     */
    private String substituteTokens(String line) {
        line = line.replace("<year>", Integer.toString(
            Calendar.getInstance().get(Calendar.YEAR)));
        if (author.trim().length() > 0) {
            line = line.replace("<name of author>",
                author.trim());
        }
        if (name.trim().length() > 0) {
            line = line.replace("<program>",
                name.trim());
        }
        return line;
    }

    /**
     * Create a new empty Java file with the license header already filled
     * in.
     *
     * @param file the file to write to
     */
    public void writeJavaLicenseHeader(final File file) {
        assert (!file.exists());

        try {
            FileWriter output = new FileWriter(file);
            output.write("/**\n");
            String licenseHeader = license.getSourceHeader();
            String [] lines = licenseHeader.split("\n");
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i];
                line = substituteTokens(line);
                output.write(" * " + line + "\n");
            }
            output.write(" */\n");
            output.close();
        } catch (IOException e) {
            // Show this exception to the user.
            new TExceptionDialog(application, e);
        }
    }

    /**
     * Get all watches in this project.
     *
     * @return a copy of the list of watches
     */
    public List<String> getWatches() {
        return new ArrayList<String>(watches);
    }

    /**
     * Set all watches in this project.
     *
     * @param watches the watches to set to
     */
    public void setWatches(final List<String> watches) {
        this.watches = new ArrayList<String>(watches);
    }

    /**
     * Propagate an exception to the UI.
     *
     * @param exception the exception to display
     */
    public void displayException(final Exception exception) {
        application.invokeLater(new Runnable() {
            public void run() {
                new TExceptionDialog(application, exception);
            }
        });
    }

}
